//
//  GraphicSystem.cpp
//  TrackingSim
//
//  Created by Garrett Manka on 11/25/11.
//  Copyright (c) 2011. All rights reserved.
//

#include <graphics/GraphicSystem.h>
#include <sim/KinematicSim.h>
#include <sim/Scenario.h>
#include <sim/Clock.h>
#include <graphics/Camera.h>
#include <list>
#include <iostream>
#include <graphics/Object.h>
#include <ResourceManager.h>

namespace Sim
{
    GraphicSystem* GraphicSystem::s_the_instance = NULL;
    bool GraphicSystem::s_instance_created = false;
    
    const int GraphicSystem::window_width = 800;
    const int GraphicSystem::window_height = 800;
    KinematicSim* GraphicSystem::s_kinematicsim = 0;
    Camera* GraphicSystem::s_camera = 0;
    
    GLuint GraphicSystem::the_scene = 0;
    std::map<std::string, GLuint> GraphicSystem::s_list_to_resource_name_map;
    
    GraphicSystem::GraphicSystem(KinematicSim* kinematicsim)
    {
        s_kinematicsim = kinematicsim;
        s_camera = new Camera();
    }
    
    GraphicSystem::~GraphicSystem()
    {
        delete s_the_instance;
        s_the_instance = NULL;
        s_instance_created = false;
    }
    
    void GraphicSystem::create(KinematicSim* kinematicsim)
    {
        if(!s_instance_created)
        {
            s_the_instance = new GraphicSystem(kinematicsim);
            s_instance_created = true;
        }
    }
    
    GraphicSystem* GraphicSystem::getGS()
    {
        if(s_instance_created)
        {
            return s_the_instance;
        }
        
        return NULL;
    }
    
    void GraphicSystem::start(int argc, char **argv)
    {
        glutInit(&argc, argv);
        glutInitDisplayMode(GLUT_DEPTH|GLUT_DOUBLE|GLUT_RGB);
        glutInitWindowSize(window_width, window_height);
        glutCreateWindow("Tracking Sim");
        
        //glutGameModeString("1024x768:32@60");
        //glutEnterGameMode();
        
        glClearColor(0.0, 0.0, 0.0, 0.0);
        glViewport(0, 0, window_width, window_height);
        
        glShadeModel(GL_SMOOTH);							// Enable Smooth Shading
        glClearColor(0.0f, 0.0f, 0.0f, 0.5f);				// Black Background
        glClearDepth(1.0f);									// Depth Buffer Setup
        glEnable(GL_DEPTH_TEST);							// Enables Depth Testing
        glDepthRange(0.0, 100.0);
        
        glEnable(GL_LIGHTING);
        
        GLfloat pos[] = { 200.0, 200.0, 200.0, 1.0 };
        GLfloat diffuse[] = { 1.0, 1.0, 1.0, 1.0 };
        GLfloat ambient[] = { 0.5, 0.5, 0.5 , 1.0 };
        GLfloat specular[] = { 1.0, 1.0, 1.0, 1.0 };
        
        glLightfv(GL_LIGHT0, GL_POSITION, pos);
        glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
        glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse);
        glLightfv(GL_LIGHT0, GL_SPECULAR, specular);
        glEnable(GL_LIGHT0);
        
        GLfloat shininess[] = { 50.0 };
        
        glMaterialfv(GL_FRONT, GL_SPECULAR, specular );
        glMaterialfv(GL_FRONT, GL_SHININESS, shininess );
        
        glCullFace(GL_BACK);
        glEnable(GL_CULL_FACE);
        
        
        glDepthFunc(GL_LEQUAL);								// The Type Of Depth Testing To Do
        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
        
        glutSetCursor(GLUT_CURSOR_NONE);
        glutWarpPointer(window_width / 2, window_height / 2);
        
        handleResize(window_width, window_height);
        
        glutDisplayFunc(display);
        glutReshapeFunc(handleResize);
        glutKeyboardFunc(keyboard);
        glutPassiveMotionFunc(mouseMoveFunc);
        glutIdleFunc(idleFunc);
        
        createDisplayLists();
        
        glutMainLoop();
    }
    
    void GraphicSystem::display()
    {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glClearDepth(1.0);
        
        glMatrixMode(GL_MODELVIEW);
        
        Player* main_player = s_kinematicsim->getScenario()->getMainPlayer();

        s_camera->orient(main_player);
        
        glLoadIdentity();

        gluLookAt(s_camera->getPosition().x(),
                  s_camera->getPosition().y(), 
                  s_camera->getPosition().z(),
                  s_camera->getFocus().x(), 
                  s_camera->getFocus().y(), 
                  s_camera->getFocus().z(), 
                  s_camera->getUp().x(), 
                  s_camera->getUp().y(), 
                  s_camera->getUp().z());
        
        Scenario* scenario = s_kinematicsim->getScenario();
        std::list< Player* >::iterator list_iter;
        
        for( list_iter = scenario->getPlayerList().begin() ; list_iter != scenario->getPlayerList().end() ; list_iter++ )
        {
            glPushMatrix();
            
            Player* player = *list_iter;
            
            //drawAxesOnPlayer(player);
            
            glMultMatrixd(player->getWorldToLocal().mat_trans());
            
            glRotated(90.0, 0.0, 1.0, 0.0);
            
            drawMesh(player->getMesh());

            glPopMatrix();
        }
        
        drawMesh("terrain2");
        
        glutSwapBuffers();
    }
    
    void GraphicSystem::drawMesh(const std::string& mesh_name)
    {
        glColor3f(1.0, 1.0, 1.0);
        
        for( unsigned int i = 0; i < ResourceManager<Object>::getRM()->getResourceList().size(); ++i )
        {
            Object* currObject = ResourceManager<Object>::getRM()->getResourceList()[i];
            
            if( currObject->getMeshGroup() == mesh_name )
            {
                glCallList( s_list_to_resource_name_map[ currObject->getName() ] );
            }
        }
    }
    
    void GraphicSystem::drawAxesOnPlayer(const Player* player)
    {
        Vector pos = player->getPosition();
        
        Vector view_point = player->getPosition() + 5 * player->getView();
        Vector up_point = player->getPosition() + 5 * player->getUp();
        Vector left_point = player->getPosition() + 5 * player->getLeft();
        
        glBegin(GL_LINES);
        
        glColor3f(1.0, 0.0, 0.0);
        glVertex3d(player->getPosition().x(), player->getPosition().y(), player->getPosition().z());
        glVertex3d(view_point.x(), view_point.y(), view_point.z());           
        
        glColor3f(0.0, 1.0, 0.0);
        glVertex3d(player->getPosition().x(), player->getPosition().y(), player->getPosition().z());
        glVertex3d(up_point.x(), up_point.y(), up_point.z());
        
        glColor3f(0.0, 0.0, 1.0);
        glVertex3d(player->getPosition().x(), player->getPosition().y(), player->getPosition().z());
        glVertex3d(left_point.x(), left_point.y(),left_point.z());
        
        glEnd();
        
        glPointSize(20.0);
        
        glBegin(GL_POINTS);
        
        glColor3f(1.0, 0.0, 0.0);
        glVertex3d(view_point.x(), view_point.y(), view_point.z());
        
        glColor3f(0.0, 1.0, 0.0);
        glVertex3d(up_point.x(), up_point.y(), up_point.z());
        
        glColor3f(0.0, 0.0, 1.0);
        glVertex3d(left_point.x(), left_point.y(), left_point.z());
        
        glEnd();
    }
    
    void GraphicSystem::createDisplayLists()
    {
        the_scene = glGenLists(ResourceManager<Object>::getRM()->getResourceList().size());
        
        //For Each Object
        for( unsigned int i = 0; i < ResourceManager<Object>::getRM()->getResourceList().size(); ++i )
        {
            Object* currObject = ResourceManager<Object>::getRM()->getResourceList()[i];
            
            glNewList(the_scene + i, GL_COMPILE);
            s_list_to_resource_name_map[ currObject->getName() ] = the_scene + i;
            
            //For Each Face
            for( unsigned int j = 0; j < currObject->getFaceList().size() ; ++j )
            {
                glBegin(GL_POLYGON);
                
                //For Each Vertex/Normal in Face
                const Face& f = currObject->getFaceList()[j];
                
                for( unsigned int k = 0 ; k < f.getVertexIndexList().size() ; ++k )
                {
                    //Protect against the cases where vertices exist but no normals
                    if( f.getNormalIndexList().size() > 0 )
                    {
                        int normal_index = f.getNormalIndexList()[k];
                        int local_normal_index = currObject->getLocalNormalIndex(normal_index);
                        
                        const Normal& n = currObject->getNormalList()[local_normal_index];
                        
                        glNormal3d( n.x(), n.y(), n.z() );
                        
                    }
                    
                    int vertex_index = f.getVertexIndexList()[k];
                    int local_vertex_index = currObject->getLocalVertexIndex(vertex_index);
                    
                    const Vertex& v = currObject->getVertexList()[local_vertex_index];
                    
                    glVertex3d( v.x(), v.y(), v.z() );
                   
                }
                
                glEnd();
            }
            
            glEndList();
        }

    }
    
    void GraphicSystem::handleResize(int w, int h)
    {
        if (h==0)										// Prevent A Divide By Zero By
        {
            h=1;										// Making Height Equal One
        }
        
        glViewport(0,0,w,h);						// Reset The Current Viewport
        
        glMatrixMode(GL_PROJECTION);						// Select The Projection Matrix
        glLoadIdentity();									// Reset The Projection Matrix
        
        // Calculate The Aspect Ratio Of The Window
        gluPerspective(90.0f,(GLfloat)w/(GLfloat)h,0.01f,100.0f);
        
        glMatrixMode(GL_MODELVIEW);							// Select The Modelview Matrix
        glLoadIdentity();
    }
    
    void GraphicSystem::moveCursor(int& x, int& y)
    {
        int windowX = glutGet(GLUT_WINDOW_X);
        int windowY = glutGet(GLUT_WINDOW_Y);
        int screenWidth = glutGet(GLUT_WINDOW_WIDTH);
        int screenHeight = glutGet(GLUT_WINDOW_HEIGHT);
        
        int screenLeft = -windowX;
        int screenTop = -windowY;
        int screenRight = screenWidth - windowX;
        int screenBottom = screenHeight - windowY;
        
        int last_x = 0;
        int last_y = 0;
        
        int delta_x = x - last_x;
        int delta_y = y - last_y;
        
        if( delta_x == 0 && delta_y == 0 ) return;
        
        if( ( x <= screenLeft + 10 )
           ||
            ( y <= screenTop + 10 )
           ||
            ( x >= screenRight - 10 )
           ||
            ( y >= screenBottom - 10 ) )
        {
            glutWarpPointer(300, 300);
            x = 300;
            y = 300;
        }
        
        
    }
    
    void GraphicSystem::mouseMoveFunc(int x, int y)
    {
        static int current_x = 0;
        static int current_y = 0;
        
        Player* p = s_kinematicsim->getScenario()->getMainPlayer();
        
        if( x  < current_x )
        {
            p->turnLeft();
            current_x = x;
        }
        else if( x > current_x )
        {
            p->turnRight();
            current_x = x;
        }
 
        if( y > current_y )
        {
            p->pitchUp();
            current_y = y;
        }
        else if( y < current_y )
        {
            p->pitchDown();
            current_y = y;
        }
        
        moveCursor(current_x, current_y);
    }
    
    void GraphicSystem::keyboard(unsigned char key, int x, int y)
    {
        Player* p = s_kinematicsim->getScenario()->getMainPlayer();
        
        switch (key) 
        {
            case 'w':
                p->accelerate();
                break;
                
            case 's':
                p->decelerate();
                break;
                
            case 'v':
                p->stop();
                break;
                
            case 'b':
                p->resume();
                break;
                
            case 'c':
                s_camera->moveUp();
                break;
                
            case 'x':
                s_camera->moveDown();
                break;
                
            case 'a':
                p->turnLeft();
                break;
                
            case 'd':
                p->turnRight();
                break;
                
            case 'q':
                exit(1);
                break;
                
            case 'f':
                p->rollLeft();
                break;
                
            case 'g':
                p->rollRight();
                break;
                
            case 'y':
                p->pitchUp();
                break;
                
            case 'h':
                p->pitchDown();
                break;
                
            case 'z':
                s_kinematicsim->getScenario()->cycleMainPlayer();
                break;
                
            default:
                break;
        }
        
        s_kinematicsim->update();
        glutPostRedisplay();
    }
    
    void GraphicSystem::idleFunc()
    {
        if( s_kinematicsim->updateNeeded() )
        {
            s_kinematicsim->update();
            glutPostRedisplay();
        }
    }
}
